/*
  Copyright 2009 Colin Prinn

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package net.recursv.motific.unittest;

import javassist.*;
import javassist.bytecode.AccessFlag;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Created by: recursv
 * Date: 31/07/2009
 * Time: 4:28:03 PM
 */
public class UnitTestGenerator {

    /**
     * Creates a default constructor for test object instantiation
     *
     * @param aClass the target class
     * @throws javassist.CannotCompileException
     *                                     if a default constructor cannot be created
     * @throws javassist.NotFoundException if the required class is not found on the classpath
     */
    void generateDefaultConstructor(CtClass aClass) throws NotFoundException, CannotCompileException {
        try {
            if (AccessFlag.PUBLIC != aClass.getConstructor("()V").getModifiers())
                throw new CannotCompileException("You must provide a public default constructor to class: " + aClass.getName());
        } catch (NotFoundException e) {
            aClass.addConstructor(CtNewConstructor.defaultConstructor(aClass));
        }
    }

    /**
     * Generate all the necessary source code for this class.
     *
     * @param aClass the target class
     * @return the number of test cases processed from the class
     * @throws NotFoundException      If required classes are not found in the classpath
     * @throws CannotCompileException If the runTests method cannot be generated
     */
    public int processClass(CtClass aClass) throws NotFoundException, CannotCompileException {
        List<CtMethod> methods = buildTestMethodList(aClass);

        // Constructor
        generateDefaultConstructor(aClass);

        // Fields
        String indexName = aClass.makeUniqueName("__");
        aClass.addField(CtField.make("private int " + indexName + " = 0;", aClass));

        // Methods
        aClass.addMethod(CtNewMethod.make("public int countTests() { return " + methods.size() + ";}", aClass));
        aClass.addMethod(CtNewMethod.make("public boolean hasMore() { return " + indexName + "<" + methods.size() + ";}", aClass));
        aClass.addMethod(CtNewMethod.make("public String getSuiteName() {return \"" + aClass.getSimpleName() + "\";}", aClass));
        if (classRequiresMethod(aClass, "finalise"))
            aClass.addMethod(CtNewMethod.make("public void finalise() throws Throwable{}", aClass));
        if (classRequiresMethod(aClass, "prepare"))
            aClass.addMethod(CtNewMethod.make("public void prepare() throws Throwable{}", aClass));
        if (classRequiresMethod(aClass, "beforeTest"))
            aClass.addMethod(CtNewMethod.make("public void beforeTest() throws Throwable{}", aClass));
        if (classRequiresMethod(aClass, "afterTest"))
            aClass.addMethod(CtNewMethod.make("public void afterTest() throws Throwable{}", aClass));

        aClass.addMethod(CtNewMethod.make(generateTestNamesMethod(methods, indexName), aClass));
        aClass.addMethod(CtNewMethod.make(generateRunTestsMethod(methods, indexName), aClass));

        // Implement interface
        addInterface(aClass);

        return methods.size();
    }

    private List<CtMethod> buildTestMethodList(CtClass aClass) throws NotFoundException {
        ArrayList<CtMethod> methods = new ArrayList<CtMethod>();
        for (CtMethod method : aClass.getMethods())
            if (isTestMethod(method))
                methods.add(method);
        return methods;
    }

    private String generateRunTestsMethod(List<CtMethod> methods, String indexName) {
        StringBuffer buffer = new StringBuffer("public void runNextTest() throws Throwable { switch(" + indexName + "++) {");
        for (int i = 0; i < methods.size(); i++)
            buffer.append("case ").append(i).append(": ").append(methods.get(i).getName()).append("();break;");
        return buffer.append("default: throw new RuntimeException();}}").toString();
    }

    private String generateTestNamesMethod(List<CtMethod> methods, String indexName) {
        StringBuffer buffer = new StringBuffer("public String getNextTestName() { switch(" + indexName + ") {");
        for (int i = 0; i < methods.size(); i++)
            buffer.append("case ").append(i).append(": return \"").append(methods.get(i).getName()).append("\";");
        return buffer.append("default: return \"\";}}").toString();
    }

    private boolean classRequiresMethod(CtClass aClass, String s) {
        boolean b = false;
        try {
            aClass.getDeclaredMethod(s);
        } catch (NotFoundException e) {
            b = true;
        }
        return b;
    }

    boolean isTestMethod(CtMethod method) throws NotFoundException {
        return methodIsPublic(method) && methodReturnsVoid(method) && methodNameIsUnreserved(method) && methodTakesZeroParameters(method);
    }

    private boolean methodTakesZeroParameters(CtMethod method) throws NotFoundException {
        return method.getParameterTypes().length == 0;
    }

    private boolean methodNameIsUnreserved(CtMethod method) throws NotFoundException {
        List<CtMethod> methods = new ArrayList<CtMethod>();
        methods.addAll(Arrays.asList(method.getDeclaringClass().getClassPool().get(Weaver.TEST_INTERFACE).getMethods()));
        methods.addAll(Arrays.asList(method.getDeclaringClass().getClassPool().get(Weaver.JAVA_LANG_OBJECT).getMethods()));

        for (CtMethod ctMethod : methods)
            if (method.getName().equals(ctMethod.getName()) && method.getSignature().equals(ctMethod.getSignature()))
                return false;
        return true;
    }

    private boolean methodReturnsVoid(CtMethod method) throws NotFoundException {
        return method.getReturnType().equals(CtClass.voidType);
    }

    private boolean methodIsPublic(CtMethod method) {
        return method.getModifiers() == AccessFlag.PUBLIC;
    }

    /**
     * Add the test interface to the class.
     *
     * @param aClass the target class
     * @throws NotFoundException if the test interface is not found
     */
    private void addInterface(CtClass aClass) throws NotFoundException {
        aClass.addInterface(aClass.getClassPool().get(Weaver.TEST_INTERFACE));
    }

    /**
     * generate the stubbed methods in the test suite iterator class
     *
     * @param iterator      the iterator class
     * @param testSuites    a list of the test suites
     * @param testCaseCount the number of test cases
     * @throws javassist.NotFoundException if the method names are wrong
     * @throws javassist.CannotCompileException
     *                                     if the compilation fails
     */
    public void processIterator(CtClass iterator, List<CtClass> testSuites, int testCaseCount) throws NotFoundException, CannotCompileException {
        ClassPool pool = iterator.getClassPool();
        String indexName = iterator.makeUniqueName("__index");
        String testsName = iterator.makeUniqueName("__tests");
        iterator.addField(CtField.make("private int " + indexName + " = -1;", iterator));
        iterator.addField(CtField.make(buildTestArray(testSuites, testsName), iterator));

        iterator.getDeclaredMethod("getNextTestSuite").setExceptionTypes(new CtClass[]{pool.get("java.lang.InstantiationException"), pool.get("java.lang.ClassNotFoundException"), pool.get("java.lang.IllegalAccessException"), pool.get("java.lang.ArrayIndexOutOfBoundsException")});
        iterator.getDeclaredMethod("getNextTestSuite").setBody("{ return ++" + indexName + "<" + testSuites.size() + " ? (" + Weaver.TEST_INTERFACE + ") Class.forName(" + testsName + "[" + indexName + "]).newInstance() : null;}");
        iterator.getDeclaredMethod("countAllTestCases").setBody("{return " + testCaseCount + ";}");
        iterator.getDeclaredMethod("countTestSuites").setBody("{return " + testSuites.size() + ";}");
    }

    /**
     * @param testSuites a list of the test suite class names
     * @param testsName  test field name
     * @return a string array with the names of all the test suites
     */
    private String buildTestArray(List<CtClass> testSuites, String testsName) {
        if (testSuites.size() == 0)
            return "private String[] " + testsName + " = new String[0];";

        StringBuffer buffer = new StringBuffer().append("private String[] ").append(testsName).append(" = new String[]{");
        for (CtClass suite : testSuites)
            buffer.append("\"").append(suite.getName()).append("\",");
        buffer.deleteCharAt(buffer.length() - 1).append("};");
        return buffer.toString();
    }

}